<!--
 * @Author: hongbin
 * @Date: 2025-04-05 21:11:14
 * @LastEditors: hongbin
 * @LastEditTime: 2025-04-05 21:12:23
 * @Description:  粒子转小车 效果还可以 应放置 three/examples/xxx
-->
<!DOCTYPE html>
<html lang="en">
	<head>
		<title>🌰🌊🚗</title>
		<meta charset="utf-8" />
		<meta
			name="viewport"
			content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0"
		/>
		<link type="text/css" rel="stylesheet" href="main.css" />
	</head>
	<body>
		<div id="info">
			<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a>
			- webgl 🌰🌊🚗
		</div>

		<script type="x-shader/x-vertex" id="vertexshader">
			varying vec3 vPos;
			varying vec3 vPositionColor;
			varying float vDepthZ;
			varying float vClipProcess;
			attribute float scale;
			attribute vec2 coord;
			attribute float toScale;
			attribute float postpone;
			attribute vec3 toPosition;
			attribute vec3 positionColor;
			uniform float iTime;
			uniform float iProcess;
			uniform sampler2D cloudTexture;

			#define PI 3.141592653589793

			mat4 rotation3d(vec3 axis, float angle) {
				axis = normalize(axis);
				float s = sin(angle);
				float c = cos(angle);
				float oc = 1.0 - c;

				return mat4(
				  oc * axis.x * axis.x + c,           oc * axis.x * axis.y - axis.z * s,  oc * axis.z * axis.x + axis.y * s,  0.0,
				  oc * axis.x * axis.y + axis.z * s,  oc * axis.y * axis.y + c,           oc * axis.y * axis.z - axis.x * s,  0.0,
				  oc * axis.z * axis.x - axis.y * s,  oc * axis.y * axis.z + axis.x * s,  oc * axis.z * axis.z + c,           0.0,
				  0.0,                                0.0,                                0.0,                                1.0
				);
			}

			vec3 bezier(vec3 p0, vec3 p1, vec3 p2, float t) {
				return mix(mix(p0, p1, t), mix(p1, p2, t), t);
			}

			void main() {

				float clipProcess = max(0., min(1.0, (iProcess - postpone ) ));

				vClipProcess = clipProcess;

				mat4 rotation = rotation3d(vec3( 0., toPosition.y, 0.), PI * 2. * clipProcess);

				vec3 centerPoint = toPosition;

				centerPoint.x *= 0.5;
				centerPoint.y *= 0.5;
				centerPoint.z *= 0.5;

				centerPoint += vec3(0, 20, 0);

				//float xProcess =  (toPosition.x + 27.13685) / 54.2737;

				vec3 curvedPos = bezier(position, centerPoint, toPosition, clipProcess);

				//vec3 transformed = mix(position,(toPosition * 2.),clipProcess);
				vec3 transformed = curvedPos;

				transformed = (rotation * vec4(transformed,1.)).xyz;
				//transformed = curvedPos;

				//float instanceID = float(gl_InstanceID);
				//vec3 pos = calculatePosition(position, toPosition, clipProcess, instanceID);
				//transformed = pos;

				vPositionColor = positionColor;
				//vPositionColor = vec3((toPosition.x + 27.13685) / 54.,0.,0.);

				transformed.y += (sin((coord.x + iTime) * 0.1) + cos((coord.y + iTime * 12.) * 0.12) + 5.) * (1.0 - (clipProcess - 0.05));

				//transformed.y += sin((coord.x / 10. + iTime)) * 1.5 + cos((coord.y / 8. + iTime)) * 1.5 + 6.;

				vec4 mvPosition = modelViewMatrix * vec4( transformed, 1.0 );

				// 不同分辨率 不同系数
				gl_PointSize = mix(scale,toScale,clipProcess) * ( 400.0 / - mvPosition.z );

				gl_Position = projectionMatrix * mvPosition;

				//vPos = mvPosition.xyz;
				vPos = position;
				vDepthZ = -mvPosition.z;
			}
		</script>

		<script type="x-shader/x-fragment" id="fragmentshader">
			varying vec3 vPos;
			varying float vDepthZ;
			varying float vClipProcess;
			varying vec3 vPositionColor;
			uniform vec3 startColor;
			uniform vec3 centerColor;
			uniform vec3 endColor;
			uniform vec3 fogColor;
			uniform float iTime;
			uniform float iProcess;
			uniform float fogDensity;
			uniform sampler2D pointTexture;
			uniform sampler2D cloudTexture;

			void main() {

				float p = (vPos.x + 150.) / 300.;

				vec3 lineColor;

				float line = cos(iTime / 5.);
				//line = 0.5;

				if (p < line)
					lineColor = mix(startColor, centerColor, p * (1. / line));
				else
					lineColor = mix(centerColor, endColor, (p - line) * (1.0 / (1.0 - line)));

				lineColor = mix(lineColor,vPositionColor,vClipProcess);

				gl_FragColor = vec4( lineColor , 1.0 );

				float ta = texture2D( pointTexture, gl_PointCoord ).a * 2.;

				if(ta < 0.05){
					discard;
				}

				//gl_FragColor.a = pow(ta,.5) * 1.;

				#include <tonemapping_fragment>
				#include <colorspace_fragment>

				// 处理雾化
				float fogFactor = 1.0 - exp( - fogDensity * fogDensity * vDepthZ * vDepthZ );

				//gl_FragColor.a = 1.;

				gl_FragColor.rgb = mix( gl_FragColor.rgb, fogColor, fogFactor );

			}
		</script>

		<script type="importmap">
			{
				"imports": {
					"three": "../build/three.module.js",
					"three/addons/": "./jsm/"
				}
			}
		</script>

		<script type="module">
			import * as THREE from "three";

			import Stats from "three/addons/libs/stats.module.js";
			import { GUI } from "three/addons/libs/lil-gui.module.min.js";
			import { OrbitControls } from "three/addons/controls/OrbitControls.js";
			import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
			import { DRACOLoader } from "three/addons/loaders/DRACOLoader.js";
			import TWEEN from "three/addons/libs/tween.module.js";

			const gui = new GUI({});

			const SEPARATION_X = 0.3,
				SEPARATION = 0.3,
				AMOUNTX = 1000,
				AMOUNTY = 200;

			const totalParticles = AMOUNTX * AMOUNTY;

			let container, stats;
			let camera, scene, renderer;

			let particles,
				count = 0;

			let mouseX = 0,
				mouseY = 0;

			let windowHalfX = window.innerWidth / 2;
			let windowHalfY = window.innerHeight / 2;

			let toPositions;
			let positionColor;
			let toScales;
			let postpones;

			const clock = new THREE.Clock();

			const iTime = { value: 0 };
			const iProcess = { value: 0 };

			const iProcessGUI = gui
				.add(iProcess, "value")
				.min(0.0)
				.max(5.0)
				.step(0.01);

			init();

			function init() {
				container = document.createElement("div");
				document.body.appendChild(container);

				camera = new THREE.PerspectiveCamera(
					75,
					window.innerWidth / window.innerHeight,
					1,
					10000
				);
				camera.position.z = 55;
				camera.position.y = 0;

				scene = new THREE.Scene();

				const numParticles = AMOUNTX * AMOUNTY;

				const positions = new Float32Array(numParticles * 3);
				toPositions = new Float32Array(numParticles * 3);
				positionColor = new Float32Array(numParticles * 3);
				const scales = new Float32Array(numParticles);
				toScales = new Float32Array(numParticles);
				postpones = new Float32Array(numParticles);
				const coords = new Float32Array(numParticles * 2);

				let i = 0,
					j = 0;

				for (let ix = 0; ix < AMOUNTX; ix++) {
					for (let iy = 0; iy < AMOUNTY; iy++) {
						// 0 * 0.3 - （1000 * 0.3） / 2
						positions[i] = ix * SEPARATION_X - (AMOUNTX * SEPARATION_X) / 2; // x

						// positions[i + 1] = 13; // y
						positions[i + 1] = 10 + (iy / AMOUNTY) * 7;

						let _SEPARATION = (AMOUNTY / (iy + 1)) * SEPARATION;
						_SEPARATION = Math.min(_SEPARATION, SEPARATION * 2);
						// let _SEPARATION =  SEPARATION;

						positions[i + 2] = iy * _SEPARATION - (AMOUNTY * _SEPARATION) / 2; // z

						scales[j] = 1;

						coords[j * 2] = ix;
						coords[j * 2 + 1] = iy;

						i += 3;
						j++;
					}
				}

				const geometry = new THREE.BufferGeometry();

				geometry.setAttribute(
					"position",
					new THREE.BufferAttribute(positions, 3)
				);

				geometry.setAttribute(
					"toPosition",
					new THREE.BufferAttribute(toPositions, 3)
				);
				geometry.setAttribute(
					"positionColor",
					new THREE.BufferAttribute(positionColor, 3)
				);

				geometry.setAttribute("scale", new THREE.BufferAttribute(scales, 1));
				geometry.setAttribute(
					"postpone",
					new THREE.BufferAttribute(postpones, 1)
				);
				geometry.setAttribute(
					"toScale",
					new THREE.BufferAttribute(toScales, 1)
				);
				geometry.setAttribute("coord", new THREE.BufferAttribute(coords, 2));

				const material = new THREE.ShaderMaterial({
					uniforms: {
						iTime,
						iProcess,
						startColor: { value: new THREE.Color(1, 0, 1) },
						centerColor: { value: new THREE.Color(0, 0, 1) },
						endColor: { value: new THREE.Color(1, 0, 0) },
						// fogColor: { value: new THREE.Color("#100920") },
						fogColor: { value: new THREE.Color("#000000") },
						fogDensity: { value: 0.02 },
						pointTexture: {
							value: new THREE.TextureLoader().load(
								"textures/sprites/spark1.png"
							),
						},
						cloudTexture: {
							value: new THREE.TextureLoader().load(
								"textures/lava/cloud.png",
								(t) => {
									t.wrapS = t.wrapT = THREE.RepeatWrapping;
								}
							),
						},
					},
					vertexShader: document.getElementById("vertexshader").textContent,
					fragmentShader: document.getElementById("fragmentshader").textContent,
					// transparent: true,
					blending: THREE.AdditiveBlending,
				});

				particles = new THREE.Points(geometry, material);
				scene.add(particles);
				// scene.add(new THREE.AxesHelper(100));

				// scene.background = new THREE.Color("#000000");
				// scene.fog = new THREE.FogExp2(0x000000, 0.005);

				//

				renderer = new THREE.WebGLRenderer({ antialias: true });
				renderer.setPixelRatio(2);
				renderer.setSize(window.innerWidth, window.innerHeight);
				renderer.setAnimationLoop(animate);
				container.appendChild(renderer.domElement);

				stats = new Stats();
				container.appendChild(stats.dom);

				// container.style.touchAction = "none";
				// container.addEventListener("pointermove", onPointerMove);

				//
				const controls = new OrbitControls(camera, renderer.domElement);

				window.addEventListener("resize", onWindowResize);

				loadCar();
			}

			function onWindowResize() {
				windowHalfX = window.innerWidth / 2;
				windowHalfY = window.innerHeight / 2;

				camera.aspect = window.innerWidth / window.innerHeight;
				camera.updateProjectionMatrix();

				renderer.setSize(window.innerWidth, window.innerHeight);
			}

			//

			function onPointerMove(event) {
				if (event.isPrimary === false) return;

				mouseX = event.clientX - windowHalfX;
				mouseY = event.clientY - windowHalfY;
			}

			//

			function animate() {
				render();
				stats.update();
				iTime.value += clock.getDelta();
			}

			gui.add(
				{
					play: () => {
						new TWEEN.Tween(iProcess)
							.to(
								{
									value: 3,
								},
								3000
							)
							.easing(TWEEN.Easing.Quadratic.Out)
							.start();
					},
				},
				"play"
			);
			gui.add(
				{
					reset: () => {
						new TWEEN.Tween(iProcess)
							.to(
								{
									value: 0,
								},
								2000
							)
							.easing(TWEEN.Easing.Quadratic.Out)
							.start();
					},
				},
				"reset"
			);

			function render() {
				// camera.position.x += (mouseX - camera.position.x) * 0.05;
				// camera.position.y += (-mouseY - camera.position.y) * 0.05;
				// camera.lookAt(scene.position);

				TWEEN.update();

				const positions = particles.geometry.attributes.position.array;
				const scales = particles.geometry.attributes.scale.array;

				let i = 0,
					j = 0;

				// for (let ix = 0; ix < AMOUNTX; ix++) {
				// 	for (let iy = 0; iy <script AMOUNTY; iy++) {
				// 		// y
				// 		positions[i + 1] =
				// 			Math.sin((ix + count) * 0.12) * 1 +
				// 			Math.cos((iy + count) * 0.13) * 1 +
				// 			5;

				// 		scales[j] = 1;

				// 		i += 3;
				// 		j++;
				// 	}
				// }

				// particles.geometry.attributes.position.needsUpdate = true;
				// particles.geometry.attributes.scale.needsUpdate = true;
				// particles.geometry.attributes.toPosition.needsUpdate = true;

				renderer.render(scene, camera);

				count += 0.1;
			}

			function loadCar() {
				const dracoLoader = new DRACOLoader();
				dracoLoader.setDecoderPath("jsm/libs/draco/gltf/");

				const loader = new GLTFLoader();
				loader.setDRACOLoader(dracoLoader);

				loader.load("models/gltf/ferrari_memo.glb", function (gltf) {
					console.log("car", gltf);

					scene.add(gltf.scene);

					gltf.scene.visible = false;

					let totalPositionCount = 0;
					let totalMeshCount = 0;

					const meshProps = [];

					gltf.scene.traverse((obj) => {
						if (obj.isMesh) {
							totalMeshCount++;
							totalPositionCount += obj.geometry.attributes.position.count;

							obj.material = new THREE.MeshStandardMaterial({
								color: "#" + Math.random().toString(16).slice(-6),
							});

							meshProps.push({
								mesh: obj,
								geometry: obj.geometry,
								positionArray: obj.geometry.attributes.position.array,
								positionCount: obj.geometry.attributes.position.count,
								curr: 0,
								color: obj.material.color,
								index: meshProps.length,
							});
						}
					});

					console.log(
						`总mesh:${totalMeshCount} , 总点数：${totalPositionCount} `,
						meshProps
					);

					const meshPropsLength = meshProps.length;

					// 随机绑定粒子对应mesh点位

					for (let i = 0; i < totalParticles; i++) {
						if (meshProps.length == 0) {
							console.warn("已经没有未同步mesh了", i);
							break;
						}

						// 随机选择一个mesh
						const randomMesh =
							meshProps[Math.floor(Math.random() * meshProps.length)];

						// 下一个待同步点的索引
						const randomPoint = randomMesh.curr++;

						const randomPoint3 = randomPoint * 3;

						// 获取顶点坐标
						const x = randomMesh.positionArray[randomPoint3];
						const y = randomMesh.positionArray[randomPoint3 + 1];
						const z = randomMesh.positionArray[randomPoint3 + 2];

						const r = randomMesh.color.r;
						const g = randomMesh.color.g;
						const b = randomMesh.color.b;

						const i3 = i * 3;
						// 赋值给粒子
						toPositions[i3] = x;
						toPositions[i3 + 1] = y;
						toPositions[i3 + 2] = z;

						positionColor[i3] = r;
						positionColor[i3 + 1] = g;
						positionColor[i3 + 2] = b;

						toScales[i] =  Math.random() / 1.5;
						// postpones[i] = Math.random() * 2;
						postpones[i] = randomMesh.index / meshPropsLength + Math.random();
						//postpones[i] = randomMesh.index / 50;

						if (randomMesh.positionCount == randomMesh.curr) {
							// 如果当前mesh的点位已经同步完了，就删除
							meshProps.splice(meshProps.indexOf(randomMesh), 1);
						}
					}

					particles.geometry.attributes.toPosition.needsUpdate = true;
					particles.geometry.attributes.positionColor.needsUpdate = true;
					particles.geometry.attributes.toScale.needsUpdate = true;
					particles.geometry.attributes.postpone.needsUpdate = true;

					console.log(particles.geometry);
				});

				scene.add(new THREE.AmbientLight(0xffffff, 10));
			}
		</script>
	</body>
</html>
